今天要來介紹的是 Windows 作業系統的執行檔 PE,理解 PE Format 可以讓我們不論是分析惡意程式或是逆向工程都可以快速地對分析目標有初步的認識,像是我們可以從 PE Format 判斷這隻 binary 是 32 bits 或 64 bits,也能判斷這隻程式是 DLL 或 EXE。我也會直接以實際的案例來分享 PE Format 可以在哪些地方被使用。
從 hexdump 可以將整個 PE File 分成這四大區塊,分別是 DOS Header、NT Header、Section Header 和 Directories:
DOS Header 是根據 _IMAGE_DOS_HEADER
這個 struct 定義的
typedef struct _IMAGE_DOS_HEADER
{
WORD e_magic; // 以 MZ 為開頭
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew; // 標示 NT Header 在檔案的起始位址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
惡意程式分析時很常會以 e_magic 的 MZ 字串來判斷程式中是否有夾帶 PE File
NT Header 則是根據 _IMAGE_DOS_HEADER
這個 struct 定義的
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // "PE/0/0"
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
其中還包含了 File Header 和 Optional Header 這兩大塊結構:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // PE可執行的架構,e.g., I386, IA64, AMD64
WORD NumberOfSections; // section數量
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; // Optional Header 的大小
WORD Characteristics; // 該 PE 是哪種類型,e.g., system file, DLL, ...
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode; // .text 段的大小
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint; // PE 被載入後開始執行的位址,需要加上 image base 才會是正確的 memory address
DWORD BaseOfCode; // .text 段的位址
DWORD BaseOfData; // .data 段的位址
DWORD ImageBase; // PE 被載入時的 memory address
DWORD SectionAlignment; // section 要對齊的大小
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; // PE 被載入後的記憶體大小,也必須是 SectionAlignment 的倍數
DWORD SizeOfHeaders; // 所有 header 的大小
DWORD CheckSum;
WORD Subsystem; // PE 會需要哪種 subsystem 啟動
WORD DllCharacteristics; // PE 啟用哪些(部分)保護也會記錄在這,e.g., DEP, SEH
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 記錄各式各樣的資訊,e.g., ImportAddressTable, ExportAddressTable, ResourceTable 等的位址、大小等資訊
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
實際上的 Data Directories 會是這樣:
記錄了非常多的資訊,最常用到的會是 Export Directory、Import Directory 和 Resource Directory 的 RVA 和 Size,可以用這些資訊分別定位到 ExportAddressTable、ImportAddressTable 和 ResourceTable,這些在靜態分析程式行為的時候非常有用
Section Headers 則是存放了這支 PE File 的程式內容,包含 code section (.text) 和 data section (.data)
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize; // 載入後的大小
} Misc;
DWORD VirtualAddress; // 加上 image base 後,為載入後的 memory address
DWORD SizeOfRawData; // 未載入前的大小
DWORD PointerToRawData; // 在 PE File 中的位置
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; // 載入後 page 的屬性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Section Headers 的擺放方式會跟 Data Directories 一樣是採用 array 的方式
大致介紹完 header 後,來看看實際上 PE 在 CFF Explorer 會是怎麼被解析的
可以看到 header 和 directory 是分開的,實際上 directory 沒有明確的擺放位址,只能根據 header 去查找,因此除了 header 本身也較為明確的規定外,PE Format 有許多部分是未被明確定義,這也導致在解析 PE File 時需要注意不同的情況。
下一篇,我將運用剛剛介紹的 PE Format,來分析如何製作最小的 PE File!